package neuralnet4ut;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Random;

public class NeuralNet4UT{
    
    Random randomGen;
    protected float learningRate = 0.01f;
    static int PATTERN_COUNT, LAYER_COUNT, INPUT_COUNT;
    int[] neuronsPerLayer;
    int currentPattern = -1;
    ArrayList<Pattern> Patterns;
    ArrayList<Neuron> Neurons;
    
    public class Neuron{
        public float w[], da, ds, dw[], output;
        public Neuron ins[], outs[];
        public int layer;
        Neuron(int _layer){
            layer = _layer;
        }
    }
    
    public class Pattern{
        public float x[], d[], E;
        Pattern(float input[]){
            x = new float[input.length + 1];
            x[0] = 1;
            for(int i = 0; i < input.length; i++)
                x[i+1] = input[i];
        }
    }
    
    public NeuralNet4UT(int[] _neuronsPerLayer, int inputCount, int patternCount){
        neuronsPerLayer = _neuronsPerLayer;
        INPUT_COUNT = inputCount;
        PATTERN_COUNT = patternCount;
        LAYER_COUNT = neuronsPerLayer.length;
        
        Patterns = new ArrayList<Pattern>();
        Neurons = new ArrayList<Neuron>();
        randomGen = new Random(System.currentTimeMillis());
        
        for(int i = 0; i < neuronsPerLayer.length; i++){
            for(int j = 0; j < neuronsPerLayer[i]; j++)
                Neurons.add(new Neuron(i));
        }
        
        for(int l = 0; l < LAYER_COUNT; l++){
            for(int i = 0; i < Neurons.size(); i++){
                if(Neurons.get(i).layer == 0 && l == 0){ // First hidden layer - only outgoing connections
                    Neurons.get(i).outs = new Neuron[neuronsPerLayer[1]];
                    for(int x = 0; x < Neurons.get(i).outs.length; x++)
                        Neurons.get(i).outs[x] = Neurons.get(x + neuronsPerLayer[0]);
                }  
                else if(Neurons.get(i).layer == LAYER_COUNT-1 && l == LAYER_COUNT-1){ // Output layer - only incoming connections
                    Neurons.get(i).ins = new Neuron[_neuronsPerLayer[l - 1]];
                    for(int y = 0; y < Neurons.get(i).ins.length; y++)
                        Neurons.get(i).ins[y] = Neurons.get(y);
                }
                else if(Neurons.get(i).layer == l){ // Hidden layer with both incoming and outgoing connections
                    Neurons.get(i).outs = new Neuron[_neuronsPerLayer[l + 1]];
                    Neurons.get(i).ins = new Neuron[_neuronsPerLayer[l - 1]];
                    for(int y = 0; y < Neurons.get(i).ins.length; y++)
                        Neurons.get(i).ins[y] = Neurons.get(y + neuronsPerLayer[l - 1]);
                    for(int x = 0; x < Neurons.get(i).outs.length; x++)
                        Neurons.get(i).outs[x] = Neurons.get(x + neuronsPerLayer[l]);
                }
            }
        }
        
        for(int i = 0; i < Neurons.size(); i++){
            if(Neurons.get(i).layer == 0)
                Neurons.get(i).w = new float[INPUT_COUNT + 1];
            else
                Neurons.get(i).w = new float[neuronsPerLayer[Neurons.get(i).layer - 1] + 1];
            for(int j = 0; j < Neurons.get(i).w.length; j++)
                Neurons.get(i).w[j] = randomGen.nextFloat() * 2 - 1;
        }
    }
    
    public void setInput(float[] _ins) {
        if (Patterns.size() < PATTERN_COUNT)
            Patterns.add(new Pattern(_ins));
        currentPattern++;
        if (currentPattern == PATTERN_COUNT)
            currentPattern = 0;
    }
    
    public float getAvgError(){
        float sum = 0;
        for(int i = 0; i < Patterns.size(); i++)
            sum += Patterns.get(i).E;
        return sum / Patterns.size();
    }
    
    public void activate() {
        float sigExp;
        for(int i = 0; i < Neurons.size(); i++){
            sigExp = Patterns.get(currentPattern).x[0] * Neurons.get(i).w[0];
            if(Neurons.get(i).layer == 0){
                for (int j = 0; j < INPUT_COUNT; j++)
                    sigExp += Neurons.get(i).w[j + 1] * Patterns.get(currentPattern).x[j + 1];;
            }
            else{
                for(int j = 0; j < Neurons.get(i).ins.length; j++)
                    sigExp += Neurons.get(i).w[j + 1] * Neurons.get(i).ins[j].output;
            }
            Neurons.get(i).output = (float)(1 / (1 + Math.pow(Math.E, sigExp)));
        }
    }
    
    public void applyBackprop(float[] expectedOutput) {
        int lastLayer = neuronsPerLayer[neuronsPerLayer.length - 1];
        if (Patterns.get(currentPattern).d == null)
            Patterns.get(currentPattern).d = expectedOutput;
        float sum = 0f;
        for(int i = 0; i < lastLayer; i++){
            sum += Math.pow(expectedOutput[i] - Neurons.get(Neurons.size() - lastLayer + i).output, 2);
            Neurons.get(Neurons.size() - lastLayer + i).da = -1 * (expectedOutput[i] - Neurons.get(Neurons.size() - lastLayer + i).output);
        }
        Patterns.get(currentPattern).E = sum * (0.5f);

        for(int i = Neurons.size() - 1; i >= 0; i--){
            Neurons.get(i).dw = new float[Neurons.get(i).w.length];
            if(Neurons.get(i).layer != 0){
                Neurons.get(i).ds = Neurons.get(i).output * (1 - Neurons.get(i).output) * Neurons.get(i).da;
                for (int w = 0; w < Neurons.get(i).w.length; w++) {
                    if (w == 0)
                        Neurons.get(i).dw[w] = Neurons.get(i).ds;
                    else
                        Neurons.get(i).dw[w] = Neurons.get(i).ds * Neurons.get(i).ins[w - 1].output;
                }
            }
            else{
                float dwSum = 0f;
                for(int x = 0; x < Neurons.get(i).outs.length; x++)
                    dwSum += Neurons.get(i).outs[x].w[i + 1] * Neurons.get(i).ds;
                for(int x = 0; x < Neurons.get(i).w.length; x++)
                    Neurons.get(i).dw[x] = Neurons.get(i).output * (1 - Neurons.get(i).output) * Patterns.get(currentPattern).x[x] * dwSum;
            }
        }

        for(Neuron neuron : Neurons){
            for (int i = 0; i < neuron.dw.length; i++)
                neuron.w[i] -= learningRate * neuron.dw[i];
        }
    }
    
    public static void train(String dataFile, int iterations, int[] _neuronsPerLayer, int inputCount) {
        ArrayList<ArrayList<Float>> data = new ArrayList<ArrayList<Float>>();
        ArrayList<Float> values = new ArrayList<Float>();
        try{
            BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(dataFile)));
            String dataLine = br.readLine();
            while(dataLine != null){
                values.clear();
                String[] inputOutput = dataLine.split(";");
                String[] input = inputOutput[0].split(",");
                String[] output = inputOutput[1].split(",");
                for(int l = 0; l < input.length; l++)
                   values.add((float)Float.parseFloat(input[l])); 
                for(int m = 0; m < output.length; m++)
                    values.add((float)Float.parseFloat(output[m])); 
                data.add((ArrayList<Float>)values.clone());
                dataLine = br.readLine();
            }
        }
        catch(IOException ioe){ioe.printStackTrace();}
        System.out.println(dataFile + " has sucessfully been read. Commencing training.");
        
        NeuralNet4UT neuralNet = new NeuralNet4UT(_neuronsPerLayer, inputCount, data.size());
        int i;
        for(i = 0; i < iterations; i++){
            for(int p = 0; p < data.size(); p++){
                float input[] = new float[inputCount];
                float output[] = new float[data.get(p).size()-inputCount];
                for(int j = 0; j < input.length; j++)
                    input[j] = data.get(p).get(j);
                for(int o = 0; o < output.length; o++)
                    output[o] = data.get(p).get(inputCount + o);
                
                neuralNet.setInput(input);
                neuralNet.activate();
                neuralNet.applyBackprop(output);
                if(p == data.size() - 1 && i%1000 == 0) System.out.println("Generation: " + i + " Average Error: " + neuralNet.getAvgError());
            }
            if(neuralNet.getAvgError() <= 0.01f)
                break;
        }
        System.out.println("Done training the neural network after " + i + " iterations!");
    }
}
